/*
 * Copyright (C) 2011 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package mockwebserver3;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

import mockwebserver3.internal.duplex.DuplexResponseBody;
import okhttp3.Headers;
import okhttp3.WebSocketListener;
import okhttp3.internal.Internal;
import okhttp3.internal.http2.Settings;

import okio.Buffer;

/**
 * A scripted response to be replayed by the mock web server.
 */
public final class MockResponse implements Cloneable {
    private static final String CHUNKED_BODY_HEADER = "Transfer-encoding: chunked";

    private String status;
    private Headers.Builder headers = new Headers.Builder();
    private Headers.Builder trailers = new Headers.Builder();

    private Buffer body;

    private long throttleBytesPerPeriod = Long.MAX_VALUE;
    private long throttlePeriodAmount = 1;
    private TimeUnit throttlePeriodUnit = TimeUnit.SECONDS;

    private SocketPolicy socketPolicy = SocketPolicy.KEEP_OPEN;
    private int http2ErrorCode = -1;

    private long bodyDelayAmount = 0;
    private TimeUnit bodyDelayUnit = TimeUnit.MILLISECONDS;

    private long headersDelayAmount = 0;
    private TimeUnit headersDelayUnit = TimeUnit.MILLISECONDS;

    private List<PushPromise> promises = new ArrayList<>();
    private Settings settings;
    private WebSocketListener webSocketListener;
    private DuplexResponseBody duplexResponseBody;

    /**
     * Creates a new mock response with an empty body.
     */
    public MockResponse() {
        setResponseCode(200);
        setHeader("Content-Length", 0);
    }

    @Override
    public MockResponse clone() {
        try {
            MockResponse result = (MockResponse) super.clone();
            result.headers = headers.build().newBuilder();
            result.promises = new ArrayList<>(promises);
            return result;
        } catch (CloneNotSupportedException e) {
            throw new AssertionError();
        }
    }

    /**
     * Returns the HTTP response line, such as "HTTP/1.1 200 OK".
     *
     * @return status
     */
    public String getStatus() {
        return status;
    }

    public MockResponse setResponseCode(int code) {
        String reason = "Mock Response";
        if (code >= 100 && code < 200) {
            reason = "Informational";
        } else if (code >= 200 && code < 300) {
            reason = "OK";
        } else if (code >= 300 && code < 400) {
            reason = "Redirection";
        } else if (code >= 400 && code < 500) {
            reason = "Client Error";
        } else if (code >= 500 && code < 600) {
            reason = "Server Error";
        }
        return setStatus("HTTP/1.1 " + code + " " + reason);
    }

    public MockResponse setStatus(String status) {
        this.status = status;
        return this;
    }

    /**
     * Returns the HTTP headers, such as "Content-Length: 0".
     *
     * @return Headers
     */
    public Headers getHeaders() {
        return headers.build();
    }

    public Headers getTrailers() {
        return trailers.build();
    }

    /**
     * Removes all HTTP headers including any "Content-Length" and "Transfer-encoding" headers that
     * were added by default.
     *
     * @return MockResponse
     */
    public MockResponse clearHeaders() {
        headers = new Headers.Builder();
        return this;
    }


    public MockResponse addHeader(String header) {
        headers.add(header);
        return this;
    }

    public MockResponse addHeader(String name, Object value) {
        headers.add(name, String.valueOf(value));
        return this;
    }
    
    public MockResponse addHeaderLenient(String name, Object value) {
        Internal.instance.addLenient(headers, name, String.valueOf(value));
        return this;
    }

    public MockResponse setHeader(String name, Object value) {
        removeHeader(name);
        return addHeader(name, value);
    }


    public MockResponse setHeaders(Headers headers) {
        this.headers = headers.newBuilder();
        return this;
    }


    public MockResponse setTrailers(Headers trailers) {
        this.trailers = trailers.newBuilder();
        return this;
    }


    public MockResponse removeHeader(String name) {
        headers.removeAll(name);
        return this;
    }

    boolean isDuplex() {
        return duplexResponseBody != null;
    }

    DuplexResponseBody getDuplexResponseBody() {
        return duplexResponseBody;
    }


    public Buffer getBody() {
        return body != null ? body.clone() : null;
    }

    public MockResponse setBody(Buffer body) {
        setHeader("Content-Length", body.size());
        this.body = body.clone(); // Defensive copy.
        return this;
    }


    public MockResponse setBody(String body) {
        return setBody(new Buffer().writeUtf8(body));
    }

    public MockResponse setBody(DuplexResponseBody duplexResponseBody) {
        this.duplexResponseBody = duplexResponseBody;
        return this;
    }

    public MockResponse setChunkedBody(Buffer body, int maxChunkSize) {
        removeHeader("Content-Length");
        headers.add(CHUNKED_BODY_HEADER);

        Buffer bytesOut = new Buffer();
        while (!body.exhausted()) {
            long chunkSize = Math.min(body.size(), maxChunkSize);
            bytesOut.writeHexadecimalUnsignedLong(chunkSize);
            bytesOut.writeUtf8("\r\n");
            bytesOut.write(body, chunkSize);
            bytesOut.writeUtf8("\r\n");
        }
        bytesOut.writeUtf8("0\r\n"); // Last chunk. Trailers follow!

        this.body = bytesOut;
        return this;
    }


    public MockResponse setChunkedBody(String body, int maxChunkSize) {
        return setChunkedBody(new Buffer().writeUtf8(body), maxChunkSize);
    }

    public SocketPolicy getSocketPolicy() {
        return socketPolicy;
    }

    public MockResponse setSocketPolicy(SocketPolicy socketPolicy) {
        this.socketPolicy = socketPolicy;
        return this;
    }

    public int getHttp2ErrorCode() {
        return http2ErrorCode;
    }

    public MockResponse setHttp2ErrorCode(int http2ErrorCode) {
        this.http2ErrorCode = http2ErrorCode;
        return this;
    }

    /**
     * Throttles the request reader and response writer to sleep for the given period after each
     * series of {@code bytesPerPeriod} bytes are transferred. Use this to simulate network behavior.
     *
     * @param bytesPerPeriod bytesPerPeriod
     * @param period period
     * @param unit TimeUnit
     * @return MockResponse
     */
    public MockResponse throttleBody(long bytesPerPeriod, long period, TimeUnit unit) {
        this.throttleBytesPerPeriod = bytesPerPeriod;
        this.throttlePeriodAmount = period;
        this.throttlePeriodUnit = unit;
        return this;
    }

    public long getThrottleBytesPerPeriod() {
        return throttleBytesPerPeriod;
    }

    public long getThrottlePeriod(TimeUnit unit) {
        return unit.convert(throttlePeriodAmount, throttlePeriodUnit);
    }

    public MockResponse setBodyDelay(long delay, TimeUnit unit) {
        bodyDelayAmount = delay;
        bodyDelayUnit = unit;
        return this;
    }

    public long getBodyDelay(TimeUnit unit) {
        return unit.convert(bodyDelayAmount, bodyDelayUnit);
    }

    public MockResponse setHeadersDelay(long delay, TimeUnit unit) {
        headersDelayAmount = delay;
        headersDelayUnit = unit;
        return this;
    }

    public long getHeadersDelay(TimeUnit unit) {
        return unit.convert(headersDelayAmount, headersDelayUnit);
    }


    public MockResponse withPush(PushPromise promise) {
        this.promises.add(promise);
        return this;
    }


    public List<PushPromise> getPushPromises() {
        return promises;
    }

    public MockResponse withSettings(Settings settings) {
        this.settings = settings;
        return this;
    }

    public Settings getSettings() {
        return settings;
    }

    public MockResponse withWebSocketUpgrade(WebSocketListener listener) {
        setStatus("HTTP/1.1 101 Switching Protocols");
        setHeader("Connection", "Upgrade");
        setHeader("Upgrade", "websocket");
        body = null;
        webSocketListener = listener;
        return this;
    }

    public WebSocketListener getWebSocketListener() {
        return webSocketListener;
    }

    @Override
    public String toString() {
        return status;
    }
}
